package topoSort;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.font.TextAttribute;
import java.awt.geom.Rectangle2D;
import java.text.AttributedString;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.Vector;

import javafx.util.Pair;

import javax.swing.ImageIcon;
import javax.swing.JPanel;
import javax.swing.event.MouseInputListener;

public class Scene extends JPanel implements MouseInputListener, Observer {
	private static final long serialVersionUID = 8890131804290426359L;

	public static final double HIT_BOX_WIDTH = 5;
	public static final double HIT_BOX_HEIGHT = 5;

	boolean m_ctrlPressed = false;
	Vertex m_movingVertex = null;
	private double m_tx = 0, m_ty = 0, m_dx = 0, m_dy = 0;
	private Vector<Edge> m_edges = new Vector<Edge>();
	private HashMap<Integer, Vertex> m_vertices = new HashMap<Integer, Vertex>();
	private HashMap<Integer, Selectable> m_selectedItems = new HashMap<Integer, Selectable>();
	private boolean pi = false;
	private Image imPi = new ImageIcon(getClass().getResource("/images/pi.png")).getImage();
	private TopoSort m_parent;

	public Scene(TopoSort parent) {
		this.m_parent = parent;
		addMouseListener(this);
	}

	public void clear() {
		m_edges.clear();
		m_vertices.clear();
		m_selectedItems.clear();
	}

	
	/**
	 * Add vertex in position (x, y) with identifier id and state s
	 * @param x
	 * @param y
	 * @param id
	 * @param s
	 */
	private void addVertex(double x, double y, int id, State s) {
		Vertex v = new Vertex(x, y);
		m_vertices.put(id, v);
		v.setColor(s.m_color);
		v.setWeight(s.m_weight);
	}

	
	/**
	 * Connect vertices with edge from vertex id1 to vertex id2
	 * @param id1
	 * @param id2
	 */
	private void addEdge(int id1, int id2) {
		Vertex from = m_vertices.get(id1);
		Vertex to = m_vertices.get(id2);
		Edge e = new Edge(from, to);
		// Don'd add same vertex twice, please
		for (Edge ee : m_edges) {
			if (ee.equals(e)) {
				return;
			}
		}
		from.addLink(e);
		to.addLink(e);
		m_edges.add(e);
		e.refreshPos();
	}

	public HashMap<Integer, Vertex> getVertices() {
		return m_vertices;
	}

	public Integer getVertexId(Vertex v) {
		for (Integer id : m_vertices.keySet()) {
			if (m_vertices.get(id) == v)
				return id;
		}
		return -1;
	}

	@Override
	public void paint(Graphics g) {
		// TODO Possible optimization: draw only visible elements
		super.paint(g);
		Graphics2D g2d = (Graphics2D) g;
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, // Anti-alias!
				RenderingHints.VALUE_ANTIALIAS_ON);

		// Draw edges between vertices
		for (Edge e : m_edges) {
			g2d.setStroke(Edge.getStroke());
			g2d.setColor(e.getColor());
			if (m_vertices.containsValue(e.getFrom())
					&& m_vertices.containsValue(e.getTo())) {
				g2d.draw(e);
				g2d.draw(e.getArrowSideL());
				g2d.draw(e.getArrowSideR());
			}
		}

		// Draw vertices
		for (Integer id : m_vertices.keySet()) {
			Vertex v = m_vertices.get(id);
			g2d.setColor(v.getColor());
			if (pi)
			{
				g2d.drawImage(imPi, (int) v.x, (int) v.y, this);
			}
			else
			{
				g2d.fill(v);
			}
		}

		g2d.setColor(Color.BLACK);
		g2d.setFont(new Font("Garamond", Font.PLAIN, 12));
		// Draw names of vertices
		for (Integer id : m_vertices.keySet()) {
			AttributedString toDraw = new AttributedString(new Integer(
					m_vertices.get(id).getWeight()).toString());
			toDraw.addAttribute(TextAttribute.BACKGROUND, Color.WHITE);
			g2d.drawString(toDraw.getIterator(),
					(int) (m_vertices.get(id).x + 7),
					(int) (m_vertices.get(id).y + 35));
		}
	}

	public Set<Integer> getSelectedIds() {
		Set<Integer> res = new HashSet<Integer>();
		for (Integer id : m_selectedItems.keySet()) {
			if (m_vertices.containsKey(id)) {
				res.add(id);
			}
		}
		return res;
	}

	public void mouseClicked(MouseEvent e) {

	}

	
	
	/**
	 * Set all vertices and edges unselected
	 */
	public void clearSelection() {
		for (Integer id : m_vertices.keySet()) {
			m_vertices.get(id).setSelected(false);
		}
		for (Edge e : m_edges) {
			e.setSelected(false);
		}
		m_selectedItems.clear();
	}

	public void mousePressed(MouseEvent e) {
		// Process multiple selection with mouse also
		// Check if click is on item, then remember coords
		// to get ready to move it in mouseDragged
		boolean hasItem = false;
		// Check if click is on item, then check if
		// it is selected and process some logics
		for (Integer id : m_vertices.keySet()) {
			Vertex vertex = m_vertices.get(id);
			// TODO Make it determine the top-drawable item
			// more id -> toper element
			if (vertex.contains(e.getPoint())) {
				hasItem = true;
				if (e.isControlDown()) {
					if (m_selectedItems.containsKey(vertex)) {
						m_selectedItems.remove(vertex);
						vertex.setSelected(false);
					} else {
						m_selectedItems.put(id, vertex);
						vertex.setSelected(true);
					}
				} else {
					clearSelection();
					m_selectedItems.put(id, vertex);
					vertex.setSelected(true);
					m_movingVertex = vertex;
				}
				m_tx = vertex.getX();
				m_ty = vertex.getY();
				m_dx = e.getX() - vertex.getX();
				m_dy = e.getY() - vertex.getY();
				break;
			}
		}
		Rectangle2D hitBox = new Rectangle2D.Double(e.getX() - HIT_BOX_WIDTH
				/ 2, e.getY() - HIT_BOX_HEIGHT / 2, HIT_BOX_WIDTH,
				HIT_BOX_HEIGHT);
		// Edges are added by their hashCode() function
		// maybe there will be collisions, but not sure
		for (Edge edge : m_edges) {
			if (edge.intersects(hitBox)) {
				hasItem = true;
				if (e.isControlDown()) {
					if (m_selectedItems.containsKey(edge)) {
						m_selectedItems.remove(edge);
						edge.setSelected(false);
					} else {
						m_selectedItems.put(edge.hashCode(), edge);
						edge.setSelected(true);
					}
				} else {
					clearSelection();
					m_selectedItems.put(edge.hashCode(), edge);
					edge.setSelected(true);
				}
			}
		}
		if (!hasItem) {
			clearSelection();
		}
		this.repaint();
		m_parent.refreshUI();
	}

	public void mouseReleased(MouseEvent e) {
		// Release the movable object
		if (m_movingVertex != null) {
			m_movingVertex.setCoords(e.getX() - m_dx, e.getY() - m_dy);
		}
		m_movingVertex = null;
		m_tx = 0;
		m_ty = 0;
		m_dx = 0;
		m_dy = 0;
		this.repaint();
		m_parent.refreshUI();
	}

	public Set<Edge> getSelectedEdges() {
		Set<Edge> res = new HashSet<Edge>();
		for (Integer id : m_selectedItems.keySet()) {
			if (!m_vertices.containsKey(id)) {
				res.add((Edge) m_selectedItems.get(id));
			}
		}
		return res;
	}

	/**
	 * @return reversed edge if it can be reversed, either return normal edge
	 */
	public Edge canReverse() {
		int count = 0;
		Edge last = null;
		for (Integer id : m_selectedItems.keySet()) {
			if (!m_vertices.containsKey(id)) {
				count++;
				last = (Edge) m_selectedItems.get(id);
			}
		}
		return ((count == 1) ? last : null);
	}

	public void mouseEntered(MouseEvent e) {
		// Do nothing, we don't care

	}

	public void mouseExited(MouseEvent e) {
		// Reset position of item that is in move to it's default
		try {
			m_movingVertex.setCoords(m_tx, m_ty);
			this.repaint();
			m_parent.refreshUI();
		} catch (NullPointerException ex) {
		} finally {
			m_movingVertex = null;
		}
	}

	public void mouseDragged(MouseEvent e) {
		// Find dx and dy from last item pos and cur pos of mouse
		// then move the item
		try {
			m_movingVertex.setCoords(e.getX() + m_dx, e.getY() + m_dy);
			repaint();
		} catch (NullPointerException ex) {
			ex.printStackTrace();
		}
	}

	public void mouseMoved(MouseEvent e) {
		// Don't actually care, maybe highlight item under cursor (after
		// all features)
		if (m_movingVertex != null) {
			m_movingVertex.x = e.getX() + m_dx;
			m_movingVertex.y = e.getY() + m_dy;
			repaint();
		}
	}

	
	public void update(Observable o, Object arg)
	{
		clearSelection();
		// Updating visual representation
		Model m = (Model) o;
		HashMap<Integer, Set<Integer>> g = m.getIncidentList();
		HashMap<Integer, State> states = m.getCurrentState();
		Set<Integer> ids = new HashSet<Integer>(m_vertices.keySet());
		int curId = m.getCurrentVertexId();
		clearEdges();
		// Add unknown vertices
		for (Integer id : g.keySet())
		{
			if (!m_vertices.containsKey(id))
			{
				// create vertex
				addVertex(500, 500, id, states.get(id));
				setVertexPos(id, new Point(500, 500));
			}
			else
			{
				Vertex vert = m_vertices.get(id);
				State state = states.get(id);
				setVertexColor(vert, state.m_color);
				setVertexWeight(vert, state.m_weight);
				vert.setBold(id == curId);
			}
			// Add their edges also
			for (Integer to : g.get(id))
			{
				if (!hasEdge(id, to))
				{
					addEdge(id, to);
				}
			}
			ids.remove(id);
		}

		// All left ids have been deleted
		for (Integer id : ids)
		{
			Vector<Edge> links = m_vertices.get(id).getLinks();
			// Remove references to this vertex
			for (Edge e : links)
			{
				e.free();
				m_edges.remove(e);
			}
			m_vertices.remove(id);
		}
		
		if(m.hasBadLink())
		{
			Pair<Integer, Integer> badLink = m.getBadLink();
			Vertex source = m_vertices.get(badLink.getKey());
			Vertex target = m_vertices.get(badLink.getValue());
			Edge badEdge = null;
			for(Edge edge: m_edges)
			{
				if(edge.getFrom().equals(source) && edge.getTo().equals(target))
				{
					badEdge = edge;
					break;
				}
			}
			if (badEdge != null)
			{
				badEdge.setColor(Color.red);
			}
		}

		this.repaint();
	}

	/**
	 * Remove all edges from scene
	 */
	@SuppressWarnings("unchecked")
	private void clearEdges() {
		for (Integer id : m_vertices.keySet()) {
			// and hope it won't crash
			Vector<Edge> links = (Vector<Edge>) m_vertices.get(id).getLinks()
					.clone();
			for (Edge e : links) {
				e.free();
				m_edges.remove(e);
			}
		}
	}

	private boolean hasEdge(Integer id, Integer to) {
		Edge ee = new Edge(m_vertices.get(id), m_vertices.get(to));
		for (Edge e : m_edges) {
			if (e.equals(ee)) {
				return true;
			}
		}
		return false;
	}

	public Integer selectionCount() {
		return m_selectedItems.size();
	}

	public boolean isSelected(Integer id) {
		return (m_selectedItems.containsKey(id));
	}

	public void setVertexColor(Vertex v, Color c) {
		v.setColor(c);
	}

	public void setVertexWeight(Vertex v, Integer w) {
		v.setWeight(w);
	}

	public void setVertexPos(Integer id, Point p) {
		m_vertices.get(id).x = p.x;
		m_vertices.get(id).y = p.y;
	}

	public int getSelectionCount() {
		return m_selectedItems.size();
	}

	public void setPicachu()
	{
		// TODO Auto-generated method stub
		pi = !pi;
		repaint();
	}
}
